// Copyright 2022 Jeroen van Nugteren

// Permission is hereby granted, free of charge, to any person obtaining a copy of this software
// and associated documentation files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:

// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
// OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

// include header
#include "dftransform.hh"

// rat common headers
#include "rat/common/error.hh"

// code specific to Rat
namespace rat{namespace dm{

	// constructor
	DFTransform::DFTransform(){
		set_name("Transform");
	}

	// constructcor
	DFTransform::DFTransform(
		const ShDistFunPr& df,
		const arma::Col<fltp>::fixed<2>& dR, 
		const fltp alpha) : DFTransform(){
		
		// set to self
		set_df(df);
		set_offset(dR);
		set_alpha(alpha);

		// check
		is_valid(true);
	}

	// factory
	ShDFTransformPr DFTransform::create(){
		return std::make_shared<DFTransform>();
	}

	// factory
	ShDFTransformPr DFTransform::create(
		const ShDistFunPr& df,
		const arma::Col<fltp>::fixed<2>& dR, 
		const fltp alpha){
		return std::make_shared<DFTransform>(df,dR,alpha);
	}


	// setters
	void DFTransform::set_df(const ShDistFunPr &df){
		df_ = df;
	}

	void DFTransform::set_offset(const arma::Col<fltp>::fixed<2>& dR){
		dR_ = dR;
	}

	void DFTransform::set_alpha(const fltp alpha){
		alpha_ = alpha;
	}


	// getters
	const ShDistFunPr& DFTransform::get_df()const{
		return df_;
	}

	const arma::Col<fltp>::fixed<2>& DFTransform::get_offset()const{
		return dR_;
	}

	fltp DFTransform::get_alpha()const{
		return alpha_;
	}

	// perimeter creation function
	ShPerimeterPr DFTransform::create_perimeter(const fltp delem)const{
		// fixed points
		const ShPerimeterPr perimeter = df_->create_perimeter(delem);

		// get mesh
		const arma::Mat<fltp>& Rn = perimeter->get_nodes();

		// original points in polar coordinates	
		const arma::Row<fltp> radius = arma::hypot(Rn.row(1)+dR_(1),Rn.row(0)+dR_(0));
		const arma::Row<fltp> theta = arma::atan2(Rn.row(1)+dR_(1),Rn.row(0)+dR_(0)) + alpha_;

		// create new perimeter
		return Perimeter::create(arma::join_vert(radius%arma::cos(theta), radius%arma::sin(theta)), perimeter->get_elements());
	}
	
	// get bounding box
	arma::Mat<fltp>::fixed<2,2> DFTransform::get_bounding() const{
		// fixed points
		const arma::Mat<fltp>::fixed<2,2> pbnd = df_->get_bounding();
		
		// original points in polar coordinates	
		const arma::Col<fltp> radius = arma::hypot(pbnd.col(1)+dR_(1),pbnd.col(0)+dR_(0));
		const arma::Col<fltp> theta = arma::atan2(pbnd.col(1)+dR_(1),pbnd.col(0)+dR_(0)) + alpha_;
		
		// create points and store
		arma::Mat<fltp>::fixed<2,2> prot = arma::join_horiz(radius%arma::cos(theta), radius%arma::sin(theta));

		// check inversion
		prot = arma::join_vert(arma::min(prot),arma::max(prot));
	
		// return rotated bounding box		
		return prot;
	}

	// get fixed points
	arma::Mat<fltp> DFTransform::get_fixed(const fltp abstol) const{
		// fixed points
		const arma::Mat<fltp> pfix = df_->get_fixed(abstol);

		// original points in polar coordinates	
		const arma::Col<fltp> radius = arma::hypot(pfix.col(1)+dR_(1),pfix.col(0)+dR_(0));
		const arma::Col<fltp> theta = arma::atan2(pfix.col(1)+dR_(1),pfix.col(0)+dR_(0)) + alpha_;

		// return fixed points
		return arma::join_horiz(radius%arma::cos(theta), radius%arma::sin(theta));
	}

	// distance function
	arma::Col<fltp> DFTransform::calc_distance(const arma::Mat<fltp> &p) const{
		// original points in polar coordinates	
		const arma::Col<fltp> radius = arma::hypot(p.col(1),p.col(0));
		const arma::Col<fltp> theta = arma::atan2(p.col(1),p.col(0)) - alpha_;

		// back to cart
		const arma::Mat<fltp> xy = arma::join_horiz(radius%arma::cos(theta) - dR_(0), radius%arma::sin(theta) - dR_(1));

		// take smallest distance
		return df_->calc_distance(xy);
	}

	// type string for serialization
	std::string DFTransform::get_type(){
		return "rat::dm::dftransform";
	}

	// validity check
	bool DFTransform::is_valid(const bool enable_throws)const{
		// check user input
		if(df_==NULL){if(enable_throws){rat_throw_line("distance function not set");} return false;};
		if(!df_->is_valid(enable_throws))return false;
		return true;
	}

	// method for serialization into json
	void DFTransform::serialize(
		Json::Value &js, 
		cmn::SList &list) const{

		// parent
		DistFun::serialize(js,list);

		// store type
		js["type"] = get_type();

		// scaling
		js["dRx"] = dR_(0);
		js["dRy"] = dR_(1);
		js["alpha"] = alpha_;

		// serialize distance functions
		js["distfun"] = cmn::Node::serialize_node(df_,list);
	}

	// method for deserialisation from json
	void DFTransform::deserialize(
		const Json::Value &js, cmn::DSList &list, 
		const cmn::NodeFactoryMap &factory_list, 
		const boost::filesystem::path &pth){

		// parent
		DistFun::deserialize(js,list,factory_list,pth);

		// variables
		set_offset({js["dRx"].ASFLTP(), js["dRy"].ASFLTP()});
		set_alpha(js["alpha"].ASFLTP());
		
		// distance function
		set_df(Node::deserialize_node<DistFun>(js["distfun"], list, factory_list, pth));
	}

}}